/**
//////////////////////////////////
*Node.java
*This class defining classes that represent nodes
*model.node.Node
*GPLv3
*@author Ferlin Alessandro
*2012.05.01
//////////////////////////////////
*/

package model.node;

import java.awt.geom.Point2D;
import java.util.LinkedList;
import java.util.ListIterator;
import java.util.Map;
import java.util.TreeMap;
import java.util.Vector;

import controller.Hypervisor;
import model.exceptions.DeathNode;
import model.messages.LocationClaim;
import model.messages.Message;
import model.messages.MessageInterface;
import model.options.Options;

public abstract class Node extends Thread{

	protected Hypervisor hypervisor;
	protected int id;
	protected Vector<Node> neighbors;
	protected LinkedList<MessageInterface> messageList;
	protected Map<Integer, Point2D.Float> storedMessage;
	protected Point2D.Float position;
	protected Integer consumedEnergy;
	protected int sentMessage;
	protected int receivedMessage;
	protected int checkedSignature;
	protected boolean isInterrupted = false;
	
	/**
	 * Constructor require position of node	
	 * @param posX
	 * @param posY
	 */
	protected Node(Hypervisor h,float posX, float posY,int idOfNode){
		hypervisor=h;
		id=idOfNode;
		neighbors=new Vector<Node>();
		messageList=new LinkedList<MessageInterface>();
		storedMessage=new TreeMap<Integer,Point2D.Float>();
		position=new Point2D.Float(posX,posY);
		consumedEnergy=0;
		sentMessage=0;
		receivedMessage=0;
		checkedSignature=0;
	};
	
	/**
	 * Get X coordinate of this Node
	 * @return X coordinate
	 */
	public float getX() {
		return position.x;
	}

	/**
	 * Get Y coordinate of this Node
	 * @return Y coordinate
	 */
	public float getY() {
		return position.y;
	}
	
	/**
	 * Get Unique Id of node
	 * @return Node Unique Id
	 */
	public int getNodeId() {
		return id;
	}

	/**
	 * Get total of sent message
	 * @return total of sent message
	 */
	public int getSentMessage() {
		return sentMessage;
	}

	/**
	 * Get total of received message
	 * @return total of received message
	 */
	public int getReceivedMessage() {
		return receivedMessage;
	}

	/**
	 * Get total of signature checks
	 * @return total of signature checks
	 */
	public int getCheckedSignature() {
		return checkedSignature;
	}

	/**
	 * Get total consumed energy 
	 * @return amount of consumed energy
	 */
	public int getConsumedEnergy() {
		return consumedEnergy;
	}

	/**
	 * Get total stored message
	 * @return total stored message
	 */
	public int getStoredMessage() {
		return storedMessage.size();
	}
	
	/**
	 * Increase consumed energy of the node
	 * @param amount value to be added
	 * @throws DeathNode if node is Death 
	 */
	public synchronized void dropEnergy(int amount) throws DeathNode{
			if(this.isNodeAlive()){
				consumedEnergy+=amount;
			}
			if(consumedEnergy >= Options.getInstance().getTotEnergy()){
				throw new DeathNode();
			}
	};

	/**
	 * True if node is still Alive (means that node still has energy) else false
	 * @return true if node still has energy, false otherwise
	 */
	public synchronized boolean isNodeAlive() {
			return consumedEnergy < Options.getInstance().getTotEnergy();
	}
	/**
	 * Return the neighbor that is the closest to the destination, if this node is the closest return this
	 * @param target
	 * @return closest to the destination
	 */
	private Node getClosestNeighbor(Point2D.Float target){
		float min=(float)position.distance(target);
		Node aux=this;
		for(int i=0; i<neighbors.size();i++){
			if(neighbors.get(i).position.distance(target) < min){
				min=(float)neighbors.get(i).position.distance(target);
				aux=neighbors.get(i);
			}		
		}
		return aux;
	};
	
	/**
	 * Check a Signature
	 * @throws DeathNode if node had finish energy
	 */
	protected void checkSignature() throws DeathNode{
		this.dropEnergy(Options.getInstance().getEnergyCheckSign());
		checkedSignature++;
	};
	
	/**
	 * Add the node n to the neighbors list, only if their distance is <= of communication radius of simulation (into options class)
	 * @param n node that will be a neighbor of node this.
	 * @return true if the node n is add as neighbor, false otherwise.
	 */
	public boolean addNeighbor(Node n){
		boolean aux=false;
		if(this.position.distance(n.getX(),n.getY()) <= Options.getInstance().getCommunicationRadius()){
			neighbors.add(n);
			aux=true;
		}
		return aux;
	};
	
	/**
	 * This method return one message, if there is no message this Thread will wait a message
	 * @return a Message
	 * @throws DeathNode will throw if this is dead
	 * @throws InterruptedException
	 */
	private synchronized MessageInterface getMessage() throws DeathNode, InterruptedException{
		if(!isNodeAlive())
			throw new DeathNode();
		while (messageList.isEmpty())
			wait();
		return messageList.removeFirst();
	};
	
	/**
	 * Send a message to node target
	 * @param node target that will receive message
	 * @param message to send
	 * @throws DeathNode if node finish energy or if it is still dead
	 */
	protected void sendMessage(Node node, MessageInterface message) throws DeathNode{
		Node first=null, second=null;
		if(getNodeId() < node.getNodeId()){
			first=this;
			second=node;
		}
		else {
			first=node;
			second=this;
		}
		//To avoid deadlock we have to synchronize node in a determinate order, determined by id of Nodes
		synchronized(first){
			synchronized(second){
				if(node.isNodeAlive()){
					if (message instanceof LocationClaim) hypervisor.getMainWindow().getSimulationTelevision().addClaim(hashCode(), node.hashCode());
					else hypervisor.getMainWindow().getSimulationTelevision().addCommunication(hashCode(), node.hashCode());
					try{
						//if this node die during the sending message, the total number of messages must be decreased
						this.dropEnergy(Options.getInstance().getEnergySending()); 
					}
					catch(DeathNode e){
						hypervisor.decreaseMessages(1, this, -1);
						throw e;
					}
					
					node.receiveMessage(message);
					sentMessage++;
				}
				else{
					throw new DeathNode();
				}
			}
		}
	};
	
	/**
	 * Receive a message, increase receiveMessage and add the message to messageList
	 * @param message
	 * @throws DeathNode if node finish energy or if it's a death node
	 */
	protected synchronized void receiveMessage(MessageInterface message) throws DeathNode{
		hypervisor.increaseMessages(this, message.getIdCreator());
		try{
			this.dropEnergy(Options.getInstance().getEnergyReceiving());
		}catch(DeathNode e){
			hypervisor.decreaseMessages(1, this, message.getIdCreator());
			notifyAll();
			throw new DeathNode();
		}
		messageList.add(message);
		receivedMessage++;
		notifyAll();
	};
	
	/**
	 * Send to all neighbor a location claim message 
	 * @throws InterruptedException
	 * @throws DeathNode if node died
	 */
	protected void sendClaims() throws InterruptedException, DeathNode{
		Node node=null;
		checkSignature();
		if (!neighbors.isEmpty()) {
			ListIterator<Node> it = neighbors.listIterator();
			while (it.hasNext()) {
				if (isInterrupted())
					throw new InterruptedException();
				node = it.next();
				try {
					sendMessage(node, new LocationClaim(this));
				} catch (DeathNode e) {
					it.remove();
				}
			}
		}
		hypervisor.claimDone();
	};
	
	/**
	 * Will send the message to the neighbor that is closest to the destination,
	 * If a node in charge of forwarding a message is more
	 * close to the destination point than any of its neighbors, it will not forward the
	 *	message (and define itself as the recipient of the message).
	 * @param message to route
	 * @return true if target of message is this node
	 * @throws DeathNode if this node dies during sent message or if it's a death node 
	 */
	protected boolean routingProcedure(Message message) throws DeathNode{
		boolean done=false;
		while(!done){
			 Node node=getClosestNeighbor(message.getDestination());
			if(node!=this){ //this node is not the Closest
				try { //try to send message
					this.sendMessage(node,message);
					done=true;
				} catch (DeathNode e) {
					if(!this.isNodeAlive()) //this node is death while trying send message
						throw new DeathNode();
					//else the receiver node is still die or was die so i remove it from neighbor
					neighbors.remove(node);
					done=true;
				}
			}
			else
				return true;
		}
		return false;
	};
	
	/**
	 * Check if there is a clone, if there is'n in the list the message store it.
	 * @return true if clone is detect, false otherwise
	 * @throws EndEnergy
	 */
	protected boolean checkForClone(MessageInterface message) throws DeathNode {
		try{
		checkSignature();
		}
		catch(DeathNode n){
			hypervisor.decreaseMessages(1, this, -1);
			throw n;
		}
		Point2D.Float point = storedMessage.get(message.getIdCreator());
		if(point != null) {
			if(point.equals(message.getPosition())){
				return false;
			}
			else{
				// clone found, notify to hypervisor
				hypervisor.endSim(true, this);
				return true;
			}
		} 
		else{
			storedMessage.put(message.getIdCreator(),message.getPosition());
			return false;
		}
	}
	
	/**
	 * The node check if have message to elaborate, if there is menages it else wait a message.
	 */
	@Override
	public void run() {
		try {
			// send claim to all neighbors
			sendClaims();
			hypervisor.waitForAllClaim();
			while (isNodeAlive() && !isInterrupted()) {
				// if simulation is paused wait
				hypervisor.waitForPause();
				// only if time delay is > 0 wait for the time in millisecond
				//the time is get from hypervisor
				if (hypervisor.getTimeDelay()>0)
					sleep(hypervisor.getTimeDelay());
				// get message or wait if there is not message
				MessageInterface message = getMessage();
				// manage message
				manageMessage(message);
				//decrease message elaborated
				hypervisor.decreaseMessages(1,this, message.getIdCreator());
			}		
			hypervisor.decreaseMessages(messageList.size(),this,-1);
		}
		catch (DeathNode e) {
			if(!isNodeAlive()){ //i am death, decrease the messages that I have not managed
				hypervisor.getMainWindow().getSimulationTelevision().endEnergy(hashCode());
				hypervisor.decreaseMessages(messageList.size(),this,-1);
			}
		}
		catch (InterruptedException e) {/*the node is interrupted, nothing to do*/}		
    }

	protected abstract void manageMessage(MessageInterface message) throws DeathNode;
}
